package p2pcdnsim.network;

import java.util.HashMap;
import java.util.TreeSet;

import p2pcdnsim.exception.PortAlreadyInUseException;
import p2pcdnsim.exception.PortUnreachableException;


import desmoj.core.simulator.Model;
import desmoj.core.simulator.SimTime;

public class TransportLayer {
	
	//TODO: improve this, it should not be here
	public static final int DEFAULT_MTU = 1500;
	
	ApplicationLayer appLayer;
	NetworkLayer netLayer;
	Model owner;
	TreeSet<Integer> portsInUse;
	TreeSet<Integer> listeningPorts;
	HashMap<Integer,ServerSocket> serverSockets;
	HashMap<Integer,DatagramSocket> datagramSockets;
	
	//Outgoing connections
	HashMap<ConnectionIdentifier,TCPConnection> connections;
	
	public TransportLayer(Model owner) {
		this.owner = owner;
		portsInUse = new TreeSet<Integer>();
		listeningPorts = new TreeSet<Integer>();
		serverSockets = new HashMap<Integer,ServerSocket>();
		datagramSockets = new HashMap<Integer,DatagramSocket>();
		connections = new HashMap<ConnectionIdentifier,TCPConnection>();
	}
	

	public void setNetworkLayer(NetworkLayer networkLayer) {
		this.netLayer = networkLayer;
		
	}

	public void setApplicationLayer(ApplicationLayer applicationLayer) {
		this.appLayer = applicationLayer;
		this.appLayer.setTransportLayer(this);
		
	}

	public ApplicationLayer getApplicationLayer() {
		return appLayer;
	}

	NetworkLayer getNetworkLayer() {
		return netLayer;
	}

	public ServerSocket getServerSocket(int port) {
		return serverSockets.get(port);
	}


	public ServerSocket createServerSocket(ApplicationProcess applicationProcess, int port) throws PortAlreadyInUseException {
		if(!listeningPorts.contains(port)) {
			listeningPorts.add(port);
			portsInUse.add(port);
			
			ServerSocket s = new ServerSocket(this,applicationProcess, applicationProcess.getNetworkAddress() ,port);
			serverSockets.put(port, s);
			return s;
		} else throw new PortAlreadyInUseException();
	}

	public DatagramSocket createDatagramSocket(ApplicationProcess applicationProcess, int port) throws PortAlreadyInUseException {
		if(!listeningPorts.contains(port)) {
			listeningPorts.add(port);
			portsInUse.add(port);
			
			DatagramSocket s = new DatagramSocket(this,applicationProcess,port);
			datagramSockets.put(port, s);
			return s;
		} else throw new PortAlreadyInUseException();
	}
	
	public DatagramSocket createDatagramSocket(ApplicationProcess applicationProcess) {
		try {
			return createDatagramSocket(applicationProcess,getFreePort());
		} catch (PortAlreadyInUseException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}
	
	public Socket createSocket(ApplicationProcess applicationProcess,
			NetworkAddress destAddress, int destPort) {
		
		int localPort = this.getFreePort();
		NetworkAddress localAddress = applicationProcess.getNetworkAddress();
		
		TCPConnection conn = new TCPConnection(this.getNetworkLayer().getModel(), this, localAddress,localPort,destAddress,destPort);
		conn.setMSS(DEFAULT_MTU);
		portsInUse.add(localPort);
		Socket s = new Socket(applicationProcess, conn);
		connections.put(new ConnectionIdentifier(localAddress,localPort,destAddress,destPort), conn);
		//System.out.println("Creating socket: put "+new ConnectionIdentifier(localAddress,localPort,destAddress,destPort));
		conn.activate(new SimTime(0));
		
		return s;
	}
	
	//Inefficient. Improve this
	private int getFreePort() {
		int port = 1025;
		while(portsInUse.contains(port)) {
			port++;
		}
		return port;
	}


	private class ConnectionIdentifier {
		NetworkAddress localAddress;
		NetworkAddress destinationAddress;
		int localPort,destinationPort;
		
		public ConnectionIdentifier(NetworkAddress localAddress,int localPort,NetworkAddress destinationAddress,int destinationPort) {
			this.localAddress = localAddress;
			this.localPort = localPort;
			this.destinationAddress = destinationAddress;
			this.destinationPort = destinationPort;
		}


		@Override
		public boolean equals(Object obj) {
			ConnectionIdentifier o = (ConnectionIdentifier)obj;
			return localAddress.equals(o.localAddress) &&
			       (localPort == o.localPort) &&
			       destinationAddress.equals(o.destinationAddress) &&
			       (destinationPort == o.destinationPort);
		}


		@Override
		public int hashCode() {
			return localAddress.hashCode() +
		       localPort +
		       destinationAddress.hashCode() +
		       destinationPort;
		}
		
		public String toString() {
			return "["+localAddress+":"+localPort+" - "+destinationAddress+":"+destinationPort+"]";
		}
		
	}


	void transmitDatagram(Datagram datagram, SimTime timestamp) {
		Packet p = new Packet(datagram.getSourceAddress(),datagram.getDestinationAddress(),timestamp);
		p.setContent(datagram);
		this.netLayer.addPacket(p);
	}


	public void receiveDatagram(Datagram content) {
		if(content instanceof TCPSegment) {
			this.receiveTCPSegment((TCPSegment)content);
			return;
		}
		if(content instanceof DatagramPacket) {
			this.receiveUDPSegment((DatagramPacket)content);
			return;
		}
		System.out.println("Other type...");
	}


	private void receiveUDPSegment(DatagramPacket content) {
		int toPort = content.getDestinationPort();
		//TODO: use network communication instead of java exception
		if(!listeningPorts.contains(toPort)) throw new RuntimeException(new PortUnreachableException());
		
		DatagramSocket ds = datagramSockets.get(toPort);
		
		ds.receiveDatagramPacket(content);
	}


	private void receiveTCPSegment(TCPSegment content) {
		ConnectionIdentifier connid = new ConnectionIdentifier(content.getDestinationAddress(),content.getDestinationPort(),content.getSourceAddress(),content.getSourcedPort());
		//System.out.println("Receiving segment: look for  "+connid);
		if(connections.containsKey(connid)) {
			//Connection already established
			connections.get(connid).receiveTCPSegment(content);
		} else {
			//System.out.println("Creating new TCP connection..."+new ConnectionIdentifier(content.getDestinationAddress(),content.getDestinationPort(),content.getSourceAddress(),content.getSourcedPort()));
			
			//Check if I am listening the destination port
			int toPort = content.getDestinationPort();
			//TODO: use network communication instead of java exception
			if(!listeningPorts.contains(toPort)) throw new RuntimeException(new PortUnreachableException());
			
			TCPConnection conn = new TCPConnection(this.getNetworkLayer().getModel(), this, content.getDestinationAddress(),content.getDestinationPort(),content.getSourceAddress(),content.getSourcedPort());
			conn.setMSS(DEFAULT_MTU);
			conn.setState(TCPConnection.LISTEN);
			
			ServerSocket ss = serverSockets.get(toPort);
			
			Socket s = new Socket(ss.getApplicationProcess(), conn);
			
			connections.put(new ConnectionIdentifier(content.getDestinationAddress(),content.getDestinationPort(),content.getSourceAddress(),content.getSourcedPort()), conn);
			conn.activate(new SimTime(0));
			
			ss.addSocket(s);
			
			conn.receiveTCPSegment(content);
		}
	}


	public void close(DatagramSocket socket) {
		int port = socket.getPort();
		if(!listeningPorts.contains(port)) listeningPorts.remove(port);
		portsInUse.remove(port);
		portsInUse.add(port);
		datagramSockets.remove(port);
	}

}
